/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.anorien.dmo.gui.optimization;

import eu.anorien.dmo.Function;
import eu.anorien.dmo.Parameter;
import eu.anorien.dmo.Server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.log4j.Logger;

/**
 *
 * @author David Miguel Antunes <davidmiguel [ at ] antunes.net>
 */
public class HillClimbingOptimization extends Optimization {

    private class GradientDescentOptimizer {

        private ArrayList<Integer> currentPoint;
        private double currentPointError;
        private Map<ArrayList<Integer>, Double> neighbors;
        private final Object searchThreadWait = new Object();
        private final Object coordinatorThreadWait = new Object();
        private int nSearchThreads;
        private boolean localTerminate;

        class SearchThread extends Thread {

            @Override
            public void run() {
                while (!terminate && !localTerminate) {
                    ArrayList<Integer> point = null;
                    synchronized (GradientDescentOptimizer.this) {
                        for (Map.Entry<ArrayList<Integer>, Double> entry : neighbors.entrySet()) {
                            if (entry.getValue() == null) {
                                point = entry.getKey();
                                neighbors.put(point, new Double(-1));
                                break;
                            }
                        }
                    }
                    if (point != null) {
                        double error = -2;
                        synchronized (errorValueCache) {
                            if (errorValueCache.get(point) != null) {
                                error = errorValueCache.get(point);
                                output.write("Cache hit.");
                            }
                        }
                        if (error == -2) {
                            error = runFunction(pointToParams(point));
                            synchronized (errorValueCache) {
                                errorValueCache.put(point, error);
                            }
                        }
                        output.write("Neighbor [" + paramsToString(pointToParams(point)) + "] has a value of " + error);
                        synchronized (GradientDescentOptimizer.this) {
                            neighbors.put(point, error);
                        }
                        synchronized (coordinatorThreadWait) {
                            coordinatorThreadWait.notifyAll();
                        }
                    } else {
                        synchronized (searchThreadWait) {
                            try {
                                if (Thread.interrupted()) {
                                    throw new InterruptedException();
                                }
                                searchThreadWait.wait(100);
                            } catch (InterruptedException ex) {
                            }
                        }
                    }
                }
                System.out.println("Search thread terminating");
            }
        }

        class CoordinatorThread extends Thread {

            @Override
            public void run() {
                outter:
                while (!terminate && !localTerminate) {
                    synchronized (GradientDescentOptimizer.this) {
                        Map.Entry<ArrayList<Integer>, Double> best = neighbors.entrySet().iterator().next();
                        for (Map.Entry<ArrayList<Integer>, Double> entry : neighbors.entrySet()) {
                            if (entry.getValue() == null || entry.getValue() == -1) {
                                continue outter;
                            } else {
                                if (entry.getValue() < best.getValue()) {
                                    best = entry;
                                }
                            }
                        }
                        // All neighbors are processed:
                        if (currentPointError >= best.getValue()) {
                            output.write("Reached local maximum of " + (1 / currentPointError) + " with params [" + paramsToString(pointToParams(currentPoint)) + "]");
                            new GradientDescentOptimizer(nSearchThreads).start();
                            localTerminate = true;
                            synchronized (searchThreadWait) {
                                searchThreadWait.notifyAll();
                            }
                            return;
                        } else {
                            currentPoint = best.getKey();
                            currentPointError = best.getValue();
                            output.write("Next point is [" + paramsToString(pointToParams(currentPoint)) + "] with a value of " + (1 / currentPointError));
                            neighbors = new HashMap<ArrayList<Integer>, Double>();
                            for (ArrayList<Integer> neighbor : neighborPoints(best.getKey())) {
                                neighbors.put(neighbor, null);
                            }
                        }
                    }
                    synchronized (searchThreadWait) {
                        searchThreadWait.notifyAll();
                    }
                    synchronized (coordinatorThreadWait) {
                        try {
                            if (Thread.interrupted()) {
                                throw new InterruptedException();
                            }
                            coordinatorThreadWait.wait();
                        } catch (InterruptedException ex) {
                        }
                    }
                }
            }
        }

        public GradientDescentOptimizer(int nSearchThreads) {
            this.nSearchThreads = nSearchThreads;
            init();
        }

        public void start() {
            for (int i = 0; i < nSearchThreads; i++) {
                Thread t = new SearchThread();
                threads.add(t);
                t.start();
            }
            Thread t = new CoordinatorThread();
            threads.add(t);
            t.start();
        }

        private void init() {
            currentPoint = newRandomPoint();
            output.write("Starting new search at point [" + paramsToString(pointToParams(currentPoint)) + "]");
            currentPointError = -1;
            neighbors = new HashMap<ArrayList<Integer>, Double>();
            for (ArrayList<Integer> neighbor : neighborPoints(currentPoint)) {
                neighbors.put(neighbor, null);
            }
        }
    }
    private static final Logger logger = Logger.getLogger(HillClimbingOptimization.class);
    private final HashMap<ArrayList<Integer>, Double> errorValueCache = new HashMap<ArrayList<Integer>, Double>();
    private HillClimbingOptimizationConfigPanel configPanel;
    private int maxSpeed = 1;

    public HillClimbingOptimization(Function function, ArrayList<Server> servers) {
        super(function, servers);
        this.function = function;
        configPanel = new HillClimbingOptimizationConfigPanel(this);
        tabbedPane.add(configPanel, 0);
        tabbedPane.setTitleAt(0, "Configuration");
        tabbedPane.setSelectedIndex(0);
    }

    private List<ArrayList<Integer>> neighborPoints(List<Integer> point) {
        if (point.isEmpty()) {
            ArrayList<ArrayList<Integer>> points = new ArrayList<ArrayList<Integer>>();
            points.add(new ArrayList<Integer>());
            return points;
        } else {
            int i = point.get(0);
            ArrayList<ArrayList<Integer>> rslt = new ArrayList<ArrayList<Integer>>();
            List<ArrayList<Integer>> neighbors = neighborPoints(point.subList(1, point.size()));

            Parameter parameter = function.getParameters().get(function.getParameters().size() - point.size());

            if (i > 0) {
                for (ArrayList<Integer> arrayList : neighbors) {
                    ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                    newArrayList.add(0, Math.max(i - (random.nextInt(maxSpeed) + 1), 0));
                    rslt.add(newArrayList);
                }
            }
            for (ArrayList<Integer> arrayList : neighbors) {
                ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                newArrayList.add(0, i);
                rslt.add(newArrayList);
            }
            if (i < parameter.getNSteps() - 1) {
                for (ArrayList<Integer> arrayList : neighbors) {
                    ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                    newArrayList.add(0, Math.min(parameter.getNSteps(), i + (random.nextInt(maxSpeed) + 1)));
                    rslt.add(newArrayList);
                }
            }
            return rslt;
        }
    }

    @Override
    public void stop() {
        configPanel.getStopButton().setEnabled(false);
        super.stop();
    }

    @Override
    public void start() {
        super.start();
        configPanel.getStartButton().setEnabled(false);
        maxSpeed = configPanel.getMaxSpeed();
        final int nGradientDescents = configPanel.getNumProbes();
        final int nServersPerGradientDescent = configPanel.getNumServersPerProbe();
        output.write("Starting optimization..." + "\n");
        
        random = new Random(configPanel.getRandomSeed());
        stopAfter = configPanel.getStopAfter();

        for (int i = 0; i < nGradientDescents; i++) {
            GradientDescentOptimizer gradientDescentOptimizer = new GradientDescentOptimizer(nServersPerGradientDescent);
            gradientDescentOptimizer.start();
        }
        configPanel.getStopButton().setEnabled(true);
    }

    @Override
    public String toString() {
        return function.getName() + " Gradient Descent";
    }
}
